package org.turbobytes.unikey.generator;

import org.turbobytes.unikey.util.*;
import org.turbobytes.unikey.util.db.jdbc.DbTemplate;
import org.turbobytes.unikey.util.db.jdbc.ConnectionCallback;
import org.turbobytes.unikey.generator.table.dao.TableIdGeneratorDao;
import org.turbobytes.unikey.generator.table.dao.impl.TableIdGeneratorJdbcDao;
import org.turbobytes.unikey.generator.table.KeyList;
import org.turbobytes.unikey.generator.table.TableConfiguration;

import javax.sql.DataSource;

import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Properties;
import java.sql.Connection;
import java.sql.SQLException;

/**
 * Generate Sequential Ids using table in a database as the backing
 * infrastructure. The Client typically should supply a datasource while
 * instantiating and configuring it. This instance uses that datasource to
 * retrieve a connection to talk to the database.
 * 
 * This means that this IdGenerator will use its own connection to talk to the
 * database instead of reusing another connection that the application is
 * already using. This is because, complications with respect to transaction
 * rollback may arise and IdGenerator (being a silo) doesn't need to participate
 * in the client applications trainsactions.
 * 
 * TODO: Make it cluster safe.
 * 
 * @author Ganeshji Marwaha
 * @since Aug 7, 2010
 */
public class TableIdGenerator extends BaseIdGenerator<Long> {

  private DataSource dataSource;

  private Properties mergedConfiguration;

  private TableConfiguration configuration;

  private TableIdGeneratorDao dao;

  Map<String, KeyList> mContext = new HashMap<String, KeyList>();

  /**
   * Default constructor that takes a datasource. It uses the default properties
   * file bundled into the library for the default configuration values
   * 
   * @param dataSource -
   *          used to talk to the database
   */
  public TableIdGenerator(DataSource dataSource) {
    this(dataSource, null);
  }

  /**
   * Constructor that takes user supplied configuration and overrides it with
   * the default configuration that is bundled with the library
   * 
   * @param dataSource -
   *          used to talk to the database
   * @param userConfiguration -
   *          user supplied configuration to override the default configurations
   */
  public TableIdGenerator(DataSource dataSource, Properties userConfiguration) {
    this(dataSource, userConfiguration, null);
  }

  /**
   * Constructor that takes not just user supplied configuration, but also takes
   * a custom dao implementation for TableIdGenerator. Without setting this dao
   * from outside (like in this constructor) it is impossible to mock
   * functionalities written for the database, thereby making this library not
   * testable. In the interest of test driven development, we have created an
   * option for the user to set this dao from outside as well.
   * 
   * @param dataSource
   * @param userConfiguration
   * @param dao
   */
  public TableIdGenerator(DataSource dataSource, Properties userConfiguration, TableIdGeneratorDao dao) {
    Assert.isNotNull(dataSource);

    // Must set these variables for defaults in properties to kick in when not
    // overridden
    // TODO: Move to an interface called Configurable making these required
    // methods over it
    defaultConfigFileName = "table_generator_default.properties";
    userConfigFileName = "table_generator.properties";

    this.dataSource = dataSource;
    this.mergedConfiguration = getMergedConfig(userConfiguration);
    this.configuration = new TableConfiguration(this.mergedConfiguration);

    if (dao == null) {
      this.dao = new TableIdGeneratorJdbcDao(this.configuration);
    } else {
      // Should not allow the externally sent dao impl to provide his own
      // configuration. That would create inconsistency
      dao.setConfiguration(this.configuration);
      this.dao = dao;
    }

    final TableIdGeneratorDao tableDao = this.dao;
    new DbTemplate(dataSource).execute(new ConnectionCallback<Object>() {
      public Object doInConnection(Connection connection) throws SQLException {
        tableDao.initDbIfNecessary(connection);
        return null;
      }
    });
  }

  /**
   * Generates Id based on Table strategy
   * 
   * @return
   * @throws SQLException
   */
  public Long generate(String context) {

    KeyList keyM = getKeysManager(context);
    long contextValue = keyM.next();

    return contextValue;
  }

  private KeyList getKeysManager(String context) {
    KeyList keyM = mContext.get(context);

    if (keyM == null) {
      keyM = new KeyList(configuration.getIncrementBy());
      mContext.put(context, keyM);
    }

    if (!keyM.hasNext()) {
      refillKeys(keyM, context);
    }
    return keyM;
  }

  public Iterator getIterator(String context) {
    KeyList keyM = getKeysManager(context);
    return keyM.iterator();
  }

  private void refillKeys(KeyList keyM, String context) {
    final TableIdGeneratorDao tableDao = this.dao;
    final String contextName = context;
    long contextValue = (Long) new DbTemplate(dataSource).execute(new ConnectionCallback<Object>() {
      public Object doInConnection(Connection connection) throws SQLException {
        return tableDao.getContextValue(connection, contextName.toUpperCase());
      }
    });

    keyM.setLastValue(contextValue);
    keyM.setCurrentValue(contextValue - configuration.getBatchSize());
  }

  public DataSource getDataSource() {
    return dataSource;
  }

  public Properties getMergedConfiguration() {
    return mergedConfiguration;
  }

  public TableConfiguration getConfiguration() {
    return configuration;
  }

  public TableIdGeneratorDao getDao() {
    return dao;
  }
}
